/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jasper.compiler;

import java.util.ArrayList;
import java.util.List;

/**
 * Represents the line and file mappings associated with a JSR-045
 * "stratum".
 *
 * @author Jayson Falkner
 * @author Shawn Bayern
 */
public class SmapStratum {

	//*********************************************************************
	// Class for storing LineInfo data

	private String stratumName;

	//*********************************************************************
	// Private state
	private List<String> fileNameList;
	private List<String> filePathList;
	private List<LineInfo> lineData;
	private int lastFileID;
	/**
	 * Constructs a new SmapStratum object for the given stratum name
	 * (e.g., JSP).
	 *
	 * @param stratumName the name of the stratum (e.g., JSP)
	 */
	public SmapStratum(String stratumName) {
		this.stratumName = stratumName;
		fileNameList = new ArrayList<String>();
		filePathList = new ArrayList<String>();
		lineData = new ArrayList<LineInfo>();
		lastFileID = 0;
	}

	//*********************************************************************
	// Constructor

	/**
	 * Adds record of a new file, by filename.
	 *
	 * @param filename the filename to add, unqualified by path.
	 */
	public void addFile(String filename) {
		addFile(filename, filename);
	}

	//*********************************************************************
	// Methods to add mapping information

	/**
	 * Adds record of a new file, by filename and path.  The path
	 * may be relative to a source compilation path.
	 *
	 * @param filename the filename to add, unqualified by path
	 * @param filePath the path for the filename, potentially relative
	 *                 to a source compilation path
	 */
	public void addFile(String filename, String filePath) {
		int pathIndex = filePathList.indexOf(filePath);
		if (pathIndex == -1) {
			fileNameList.add(filename);
			filePathList.add(filePath);
		}
	}

	/**
	 * Combines consecutive LineInfos wherever possible
	 */
	public void optimizeLineSection() {

/* Some debugging code
        for (int i = 0; i < lineData.size(); i++) {
            LineInfo li = (LineInfo)lineData.get(i);
            System.out.print(li.toString());
        }
*/
		//Incorporate each LineInfo into the previous LineInfo's
		//outputLineIncrement, if possible
		int i = 0;
		while (i < lineData.size() - 1) {
			LineInfo li = lineData.get(i);
			LineInfo liNext = lineData.get(i + 1);
			if (!liNext.lineFileIDSet
					&& liNext.inputStartLine == li.inputStartLine
					&& liNext.inputLineCount == 1
					&& li.inputLineCount == 1
					&& liNext.outputStartLine
					== li.outputStartLine
					+ li.inputLineCount * li.outputLineIncrement) {
				li.setOutputLineIncrement(
						liNext.outputStartLine
								- li.outputStartLine
								+ liNext.outputLineIncrement);
				lineData.remove(i + 1);
			} else {
				i++;
			}
		}

		//Incorporate each LineInfo into the previous LineInfo's
		//inputLineCount, if possible
		i = 0;
		while (i < lineData.size() - 1) {
			LineInfo li = lineData.get(i);
			LineInfo liNext = lineData.get(i + 1);
			if (!liNext.lineFileIDSet
					&& liNext.inputStartLine == li.inputStartLine + li.inputLineCount
					&& liNext.outputLineIncrement == li.outputLineIncrement
					&& liNext.outputStartLine
					== li.outputStartLine
					+ li.inputLineCount * li.outputLineIncrement) {
				li.setInputLineCount(li.inputLineCount + liNext.inputLineCount);
				lineData.remove(i + 1);
			} else {
				i++;
			}
		}
	}

	/**
	 * Adds complete information about a simple line mapping.  Specify
	 * all the fields in this method; the back-end machinery takes care
	 * of printing only those that are necessary in the final SMAP.
	 * (My view is that fields are optional primarily for spatial efficiency,
	 * not for programmer convenience.  Could always add utility methods
	 * later.)
	 *
	 * @param inputStartLine      starting line in the source file
	 *                            (SMAP <tt>InputStartLine</tt>)
	 * @param inputFileName       the filepath (or name) from which the input comes
	 *                            (yields SMAP <tt>LineFileID</tt>)  Use unqualified names
	 *                            carefully, and only when they uniquely identify a file.
	 * @param inputLineCount      the number of lines in the input to map
	 *                            (SMAP <tt>LineFileCount</tt>)
	 * @param outputStartLine     starting line in the output file
	 *                            (SMAP <tt>OutputStartLine</tt>)
	 * @param outputLineIncrement number of output lines to map to each
	 *                            input line (SMAP <tt>OutputLineIncrement</tt>).  <i>Given the
	 *                            fact that the name starts with "output", I continuously have
	 *                            the subconscious urge to call this field
	 *                            <tt>OutputLineExcrement</tt>.</i>
	 */
	public void addLineData(
			int inputStartLine,
			String inputFileName,
			int inputLineCount,
			int outputStartLine,
			int outputLineIncrement) {
		// check the input - what are you doing here??
		int fileIndex = filePathList.indexOf(inputFileName);
		if (fileIndex == -1) // still
			throw new IllegalArgumentException(
					"inputFileName: " + inputFileName);

		//Jasper incorrectly SMAPs certain Nodes, giving them an
		//outputStartLine of 0.  This can cause a fatal error in
		//optimizeLineSection, making it impossible for Jasper to
		//compile the JSP.  Until we can fix the underlying
		//SMAPping problem, we simply ignore the flawed SMAP entries.
		if (outputStartLine == 0)
			return;

		// build the LineInfo
		LineInfo li = new LineInfo();
		li.setInputStartLine(inputStartLine);
		li.setInputLineCount(inputLineCount);
		li.setOutputStartLine(outputStartLine);
		li.setOutputLineIncrement(outputLineIncrement);
		if (fileIndex != lastFileID)
			li.setLineFileID(fileIndex);
		lastFileID = fileIndex;

		// save it
		lineData.add(li);
	}

	/**
	 * Returns the name of the stratum.
	 */
	public String getStratumName() {
		return stratumName;
	}

	//*********************************************************************
	// Methods to retrieve information

	/**
	 * Returns the given stratum as a String:  a StratumSection,
	 * followed by at least one FileSection and at least one LineSection.
	 */
	public String getString() {
		// check state and initialize buffer
		if (fileNameList.size() == 0 || lineData.size() == 0)
			return null;

		StringBuilder out = new StringBuilder();

		// print StratumSection
		out.append("*S " + stratumName + "\n");

		// print FileSection
		out.append("*F\n");
		int bound = fileNameList.size();
		for (int i = 0; i < bound; i++) {
			if (filePathList.get(i) != null) {
				out.append("+ " + i + " " + fileNameList.get(i) + "\n");
				// Source paths must be relative, not absolute, so we
				// remove the leading "/", if one exists.
				String filePath = filePathList.get(i);
				if (filePath.startsWith("/")) {
					filePath = filePath.substring(1);
				}
				out.append(filePath + "\n");
			} else {
				out.append(i + " " + fileNameList.get(i) + "\n");
			}
		}

		// print LineSection
		out.append("*L\n");
		bound = lineData.size();
		for (int i = 0; i < bound; i++) {
			LineInfo li = lineData.get(i);
			out.append(li.getString());
		}

		return out.toString();
	}

	@Override
	public String toString() {
		return getString();
	}

	/**
	 * Represents a single LineSection in an SMAP, associated with
	 * a particular stratum.
	 */
	public static class LineInfo {
		private int inputStartLine = -1;
		private int outputStartLine = -1;
		private int lineFileID = 0;
		private int inputLineCount = 1;
		private int outputLineIncrement = 1;
		private boolean lineFileIDSet = false;

		/**
		 * Sets InputStartLine.
		 */
		public void setInputStartLine(int inputStartLine) {
			if (inputStartLine < 0)
				throw new IllegalArgumentException("" + inputStartLine);
			this.inputStartLine = inputStartLine;
		}

		/**
		 * Sets OutputStartLine.
		 */
		public void setOutputStartLine(int outputStartLine) {
			if (outputStartLine < 0)
				throw new IllegalArgumentException("" + outputStartLine);
			this.outputStartLine = outputStartLine;
		}

		/**
		 * Sets lineFileID.  Should be called only when different from
		 * that of prior LineInfo object (in any given context) or 0
		 * if the current LineInfo has no (logical) predecessor.
		 * <tt>LineInfo</tt> will print this file number no matter what.
		 */
		public void setLineFileID(int lineFileID) {
			if (lineFileID < 0)
				throw new IllegalArgumentException("" + lineFileID);
			this.lineFileID = lineFileID;
			this.lineFileIDSet = true;
		}

		/**
		 * Sets InputLineCount.
		 */
		public void setInputLineCount(int inputLineCount) {
			if (inputLineCount < 0)
				throw new IllegalArgumentException("" + inputLineCount);
			this.inputLineCount = inputLineCount;
		}

		/**
		 * Sets OutputLineIncrement.
		 */
		public void setOutputLineIncrement(int outputLineIncrement) {
			if (outputLineIncrement < 0)
				throw new IllegalArgumentException("" + outputLineIncrement);
			this.outputLineIncrement = outputLineIncrement;
		}

		/**
		 * Retrieves the current LineInfo as a String, print all values
		 * only when appropriate (but LineInfoID if and only if it's been
		 * specified, as its necessity is sensitive to context).
		 */
		public String getString() {
			if (inputStartLine == -1 || outputStartLine == -1)
				throw new IllegalStateException();
			StringBuilder out = new StringBuilder();
			out.append(inputStartLine);
			if (lineFileIDSet)
				out.append("#" + lineFileID);
			if (inputLineCount != 1)
				out.append("," + inputLineCount);
			out.append(":" + outputStartLine);
			if (outputLineIncrement != 1)
				out.append("," + outputLineIncrement);
			out.append('\n');
			return out.toString();
		}

		@Override
		public String toString() {
			return getString();
		}
	}

}
